#!/usr/bin/env python3

import os
import signal
import subprocess
import sys

from shell_helpers import LF
import common
import lkmc.import_path
import thread_pool

class GdbTestcase:
    def __init__(
        self,
        source_path,
        test_script_path,
        cmd,
        verbose=False
    ):
        '''
        :param verbose: if True, print extra debug information to help understand
                    why a test is not working
        '''
        self.prompt = '\(gdb\) '
        self.source_path = source_path
        import pexpect
        self.child = pexpect.spawn(
            cmd[0],
            cmd[1:],
            encoding='utf-8'
        )
        if verbose:
            self.child.logfile = sys.stdout
        self.child.setecho(False)
        self.child.waitnoecho()
        self.child.expect(self.prompt)
        test = lkmc.import_path.import_path(test_script_path)
        exception = None
        try:
            test.test(self)
        except Exception as e:
            exception = e
        self.child.sendcontrol('d')
        self.child.close()
        self.exception = exception

    def before(self):
        return self.child.before.rstrip()

    def continue_to(self, lineid):
        line_number = self.find_line(lineid)
        self.sendline('tbreak {}'.format(line_number))
        self.sendline('continue')

    def get_int(self, int_id):
        self.sendline('printf "%d\\n", {}'.format(int_id))
        return int(self.before())

    def get_float(self, float_id):
        self.sendline('printf "%f\\n", {}'.format(float_id))
        return float(self.before())

    def find_line(self, lineid):
        '''
        Search for the first line that contains a comment line
        that ends in /* test-gdb-<lineid> */ and return the line number.
        '''
        lineend = '/* test-gdb-' + lineid + ' */'
        with open(self.source_path, 'r') as f:
            for i, line in enumerate(f):
                if line.rstrip().endswith(lineend):
                    return i + 1
        return -1

    def sendline(self, line):
        self.child.sendline(line)
        self.child.expect(self.prompt)

class Main(common.LkmcCliFunction):
    def __init__(self):
        super().__init__(description='''\
Connect with GDB to an emulator to debug Linux itself
''')
        self.add_argument(
            '--after',
            default='',
            help='''Pass extra arguments to GDB, to be appended after all other arguments.'''
        )
        self.add_argument(
            '--before',
            default='',
            help='''Pass extra arguments to GDB to be prepended before any of the arguments passed by this script.'''
        )
        self.add_argument(
            '--continue',
            default=True,
            help='''\
Run `continue` in GDB after connecting.
* https://cirosantilli.com/linux-kernel-module-cheat#gdb-step-debug-early-boot
* https://cirosantilli.com/linux-kernel-module-cheat#freestanding-programs
* https://cirosantilli.com/linux-kernel-module-cheat#baremetal-gdb-step-debug
'''
        )
        self.add_argument(
            '--gdbserver',
            default=False,
            help='''https://cirosantilli.com/linux-kernel-module-cheat#gdbserver'''
        )
        self.add_argument(
            '--kgdb',
            default=False,
            help='''https://cirosantilli.com/linux-kernel-module-cheat#kgdb'''
        )
        self.add_argument(
            '--lxsymbols',
            default=True,
            help='''\
Use the Linux kernel lxsymbols GDB script.
Only enabled by default when debugging the Linux kernel, not on userland or baremetal.
* https://cirosantilli.com/linux-kernel-module-cheat#gdb-step-debug-kernel-module
* https://cirosantilli.com/linux-kernel-module-cheat#bypass-lx-symbols
'''
        )
        self.add_argument(
            '--sim',
            default=False,
            help='''\
Use the built-in GDB CPU simulator.
https://cirosantilli.com/linux-kernel-module-cheat#gdb-builtin-cpu-simulator
'''
        )
        self.add_argument(
            '--test',
            default=False,
            help='''\
Run an expect test case instead of interactive usage. For baremetal and userland,
the script is a .py file next to the source code.
'''
        )
        self.add_argument(
            'break_at',
            nargs='?',
            help='''\
If given, break at the given expression, e.g. `main`. You will be left there automatically
by default due to --continue if this breakpoint is reached.
'''
        )

    def timed_main(self):
        after = self.sh.shlex_split(self.env['after'])
        before = self.sh.shlex_split(self.env['before'])
        no_continue = not self.env['continue']
        if self.env['test']:
            no_continue = True
            before.extend([
                '-q', LF,
                '-nh', LF,
                '-ex', 'set confirm off', LF
            ])
        elif self.env['verbose']:
            # The output of this would affect the tests.
            # https://stackoverflow.com/questions/13496389/gdb-remote-protocol-how-to-analyse-packets
            # Also be opinionated and set remotetimeout to allow you to step debug the emulator at the same time.
            before.extend([
                '-ex', 'set debug remote 1', LF,
                '-ex', 'set remotetimeout 99999', LF,
            ])
        if self.env['break_at'] is not None:
            break_at = ['-ex', 'break {}'.format(self.env['break_at']), LF]
        else:
            break_at = []
        if self.env['userland']:
            image = self.env['image']
            linux_full_system = False
            if self.env['gdbserver']:
                before.extend([
                    '-ex', 'set sysroot {}'.format(self.env['buildroot_staging_dir']),
                ])
        elif self.env['baremetal']:
            image = self.env['image']
            linux_full_system = False
        else:
            image = self.env['vmlinux']
            linux_full_system = True
        cmd = (
            [self.env['gdb_path'], LF] +
            before
        )
        if linux_full_system:
            cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(self.env['linux_build_dir']), LF])
        if self.env['sim']:
            target = 'sim'
        else:
            if self.env['gdbserver']:
                port = self.env['qemu_hostfwd_generic_port']
            elif self.env['kgdb']:
                port = self.env['extra_serial_port']
            else:
                port = self.env['gdb_port']
            target = 'remote localhost:{}'.format(port)
        cmd.extend([
            '-ex', 'file {}'.format(image), LF,
            '-ex', 'target {}'.format(target), LF,
        ])
        if not self.env['kgdb']:
            cmd.extend(break_at)
        if not no_continue:
            # ## lx-symbols
            #
            # ### lx-symbols after continue
            #
            # lx symbols must be run after continue.
            #
            # running it immediately after the connect on the bootloader leads to failure,
            # likely because kernel structure on which it depends are not yet available.
            #
            # With this setup, continue runs, and lx-symbols only runs when a break happens,
            # either by hitting the breakpoint, or by entering Ctrl + C.
            #
            # Sure, if the user sets a break on a raw address of the bootloader,
            # problems will still arise, but let's think about that some other time.
            #
            # ### lx-symbols autoload
            #
            # The lx-symbols commands gets loaded through the file vmlinux-gdb.py
            # which gets put on the kernel build root when python debugging scripts are enabled.
            cmd.extend(['-ex', 'continue', LF])
            if self.env['lxsymbols'] and linux_full_system:
                cmd.extend(['-ex', 'lx-symbols {}'.format(self.env['kernel_modules_build_subdir']), LF])
        cmd.extend(after)
        if self.env['test']:
            self.sh.print_cmd(cmd)
            if not self.env['dry_run']:
                exception = GdbTestcase(
                    self.env['source_path'],
                    os.path.splitext(self.env['source_path'])[0] + '.py',
                    self.sh.strip_newlines(cmd),
                    verbose=self.env['verbose'],
                ).exception
                if exception is None:
                    exit_status = 0
                else:
                    exit_status = 1
                    self.log_info(thread_pool.ThreadPool.exception_traceback_string(exception))
                return exit_status
        else:
            # I would rather have cwd be out_rootfs_overlay_dir,
            # but then lx-symbols cannot fine the vmlinux and fails with:
            # vmlinux: No such file or directory.
            return self.sh.run_cmd(
                cmd,
                cmd_file=os.path.join(self.env['run_dir'], 'run-gdb.sh'),
                cwd=self.env['linux_build_dir']
            )

if __name__ == '__main__':
    Main().cli()
